#include "date.h"
#include "parser.h"

//@author A0101728M
//Code can be improved by using get functions and set functions.

const int MINIMUM_YEAR = 1970;
const int MAXIMUM_YEAR = 9999;	
const char* ampm [] = {"am", "AM", "PM", "pm"};
const static int BUFFER_SIZE = 512;
const static string TODAY = "today";
const static string NEXT = "next";
const static string TOMORROW = "tomorrow";
const static string YESTERDAY = "yesterday";
const static string DAY_AFTER_TMR = "day after tomorrow";
const static string INVALID_DATE_TIME = "";
const static string MONTH_DATE_REP = "%s/%s/%d";
const char* DAYS_OF_WEEK [] = { "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" };
const char* DAYS_OF_WEEK_SHORT [] = { "Sun", "Mon", "Tue", "Wed", "Thurs", "Fri", "Sat" };
const char* MONTHS_OF_YEAR [] = { "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" };
const char* MONTHS_OF_YEAR_SHORT [] ={"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov","Dec" };
const int DAYS_OF_MONTHS [] = { 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
const int DEFAULT_TIME [] = {23, 59};

//Misc
//Return current system time as a Date.
Date Date::now(){
	return Date(boost::posix_time::second_clock::local_time());
}

//Constructors
Date::Date(int year, int month, int date, int hour, int minute){
	Date::timeinfo = setAllFields(year, month, date, hour,minute);
}

Date::Date(boost::posix_time::ptime time){
	Date::timeinfo = time;
}

Date::Date(){
	Date::timeinfo = boost::posix_time::ptime(
		boost::gregorian::date(
			boost::gregorian::greg_year(1970),
			boost::gregorian::greg_month(1),
			boost::gregorian::greg_day(1)),
		boost::posix_time::time_duration(long(0), long(0), long(0), long(0)));
}

//Private functions
void Date::updateDate(boost::gregorian::date newDate){
	Date::timeinfo = boost::posix_time::ptime(newDate, Date::timeinfo.time_of_day());
}

void Date::updateTime(boost::posix_time::time_duration newTime){
	Date::timeinfo = boost::posix_time::ptime(Date::timeinfo.date(), newTime);
}

//Set methods
boost::posix_time::ptime Date::setAllFields(int year, int month, int date, int hour, int minute) {
	return boost::posix_time::ptime(
		boost::gregorian::date(
		boost::gregorian::greg_year(long(year)),
		boost::gregorian::greg_month(long(month)),
		boost::gregorian::greg_day(long(date))),
		boost::posix_time::time_duration(long(hour), long(minute), long(0), long(0)));
}

void Date::set(boost::posix_time::ptime time){
	Date::timeinfo = time;
}

void Date::setHour(int inputHour){
	if (inputHour < 0 || inputHour > 23) throw "invalid setHour";
	assert(inputHour >= 0 && inputHour <= 23);
	boost::posix_time::time_duration hour = boost::posix_time::hours(long(inputHour));
	boost::posix_time::time_duration minute = boost::posix_time::minutes(Date::timeinfo.time_of_day().minutes());
	Date::timeinfo = boost::posix_time::ptime(Date::timeinfo.date(), hour + minute);
}

void Date::setMinute(int inputMinute){
	if (inputMinute < 0 || inputMinute > 59) throw "invalid setMinute";
	assert(inputMinute >= 0 && inputMinute <= 59);
	boost::posix_time::time_duration hour = boost::posix_time::minutes(Date::timeinfo.time_of_day().hours());
	boost::posix_time::time_duration minute = boost::posix_time::minutes(long(inputMinute));
	Date::timeinfo = boost::posix_time::ptime(Date::timeinfo.date(), hour + minute);
}

void Date::setDate(int inputDate){
	int currentMonth = getMonth();
	int maxDate = DAYS_OF_MONTHS[currentMonth];
	if (inputDate < 0 || inputDate > maxDate) throw "invalid setDate";
	assert(inputDate >= 0 && inputDate <= maxDate);
	boost::gregorian::greg_year year = Date::timeinfo.date().year();
	boost::gregorian::greg_month month = Date::timeinfo.date().month();
	boost::gregorian::date date = boost::gregorian::date(year, month, boost::gregorian::greg_day(inputDate));
	Date::timeinfo = boost::posix_time::ptime(date, Date::timeinfo.time_of_day());
}

void Date::setMonth(int inputMonth){
	if (inputMonth < 0 || inputMonth >= 12) throw "invalid setMonth"
	assert(inputMonth >= 0 && inputMonth < 12);
	boost::gregorian::greg_year year = Date::timeinfo.date().year();
	boost::gregorian::greg_day day = Date::timeinfo.date().day();
	boost::gregorian::date date = boost::gregorian::date(year, boost::gregorian::greg_month(inputMonth), day);
	Date::timeinfo = boost::posix_time::ptime(date, Date::timeinfo.time_of_day());
}

void Date::setYear(int inputYear){
	if (inputYear < 1970) throw "invalid setYear";
	assert(inputYear >= 1970);
	boost::gregorian::greg_month month = Date::timeinfo.date().month();
	boost::gregorian::greg_day day = Date::timeinfo.date().day();
	boost::gregorian::date date = boost::gregorian::date(boost::gregorian::greg_year(inputYear), month, day);
	Date::timeinfo = boost::posix_time::ptime(date, Date::timeinfo.time_of_day());
}

//Get methods
boost::posix_time::ptime Date::get() const{
	return Date::timeinfo;
}

//Month: 0-11
int Date::getMonth(){
	return Date::timeinfo.date().month().as_number() - 1;
}

//YYYY eg 2013
int Date::getYear(){
	return Date::timeinfo.date().year();
}

//Day number of the month
int Date::getDate(){
	return Date::timeinfo.date().day().as_number();
}

//0-6: Sunday-Saturday
int Date::getDay(){
	return Date::timeinfo.date().day_of_week();
}

//0-23
int Date::getHour(){
	return int(Date::timeinfo.time_of_day().hours());
}

//0-59
int Date::getMinute(){
	return int(Date::timeinfo.time_of_day().minutes());
}

//@author ?
//Returns date/time as a displayable string.
//The default simple string.
std::string Date::toString(){
	static std::locale loc(std::locale::classic(),new boost::posix_time::time_facet("%Y/%b/%d %H:%M"));
	std::basic_stringstream<char> wss;
	wss.imbue(loc);
	wss << this->timeinfo;
	return std::string(wss.str());
}

std::string Date::toDisplayString(){
	static std::locale loc;

	if (this->getYear() != Date::now().getYear()){
		loc = std::locale(std::locale::classic(), new boost::posix_time::time_facet("%d %b '%y %H:%M"));
	}
	else{
		loc = std::locale(std::locale::classic(), new boost::posix_time::time_facet("%d %b %H:%M"));
	}
	std::basic_stringstream<char> wss;
	wss.imbue(loc);
	wss << this->timeinfo;
	return std::string(wss.str());
}


std::string Date::toStringIncSec() {
	static std::locale loc(std::locale::classic(),new boost::posix_time::time_facet("%Y/%b/%d %H:%M"));
	std::basic_stringstream<char> wss;
	wss.imbue(loc);
	wss << this->timeinfo;	
	return to_simple_string(this->timeinfo);
}

// @author A0105541U
int Date::checkMonthLetter(std::string s) {
	int m = -1;
	int len = s.length();
	if (s == INVALID_DATE_TIME || len>10) return m;
	if (s[0]>='a' && s[0]<='z') s[0] -= 32;
	for (int i = 0; i < 12; i++) {
		if (s.compare(MONTHS_OF_YEAR[i]) == 0 || s.compare(MONTHS_OF_YEAR_SHORT[i]) == 0) {
			m = i+1;
			break;
		}
	}
	return m;
}

// @author A0105541U
int Date::parseInt(std::string s) {
	char buffer[BUFFER_SIZE];
	while (s[0] == '0' && s.length()>1) {
		s = s.substr(1);
	}
	int len = s.length();
	int ans = atoi(s.c_str());
	//std::string check = std::to_string((long double)ans);
	sprintf_s(buffer, "%d", ans);
	std::string check = buffer;
	if (check.length() != len || ans<0) return -1;
	else return ans;
}

// @author A0105541U
std::string Date::appendZero(int i) {
	char buffer[BUFFER_SIZE];
	sprintf_s(buffer, "%d", i);
	std::string ans = buffer;
	if (i<10) return "0"+ans;
	else return ans;
}

// @author A0105541U
bool Date::isLeapYear(int y) {
	if (y%4 != 0 ) return false;
	if (y%100 == 0 & y%400 !=0) return false;
	return true;
}

// @author A0105541U
std::string Date::parseTime(std::string s) {
	// 5:2 invalid
	// 5:02 valid
	// 5:02pm valid
	// 5pm valid
	int pos = s.find(':'), h, m;
	std::string st;
	int len = s.length();
	if (len < 3) return INVALID_DATE_TIME;

    // check if am pm exists
	int offsetHour = -1;
	st = s.substr(len-2, 2);
	for (int i = 0; i < 4; i ++ ) {
		if (st == ampm[i] ) {
			offsetHour = (i/2)*12;
			break;
		}
	}
	
	if (pos == std::string::npos) {
		// 5pm 17pm
		if (offsetHour!=-1) {
			h = parseInt(s.substr(0, len-2));
			if (h == -1 || h > 12) {
				return INVALID_DATE_TIME;
			} else if (h == 12) {
				// 12PM shall be 12 noon, hence offsetHour should be 0
				// 12AM shall be 12 midnight, hence offsetHour should be -12
				offsetHour = offsetHour - 12;
			}
			h = h + offsetHour;
			return appendZero(h)+":00";
		} else {
			// INVALID_DATE_TIME
			return INVALID_DATE_TIME;
		}
	} else {
		std::string st = s.substr(0, pos);
		h = parseInt(st);
		st = s.substr(pos+1);
		if (offsetHour != -1) st = st.substr(0, st.length()-2);
		m= parseInt(st);
		if (h>=0 && m>=0 && h<24 && m<60) {
			if (offsetHour != -1) {
				if (h == 12) {
					// 12:30pm is 12:30, 12:30am is 00:30
					offsetHour -= 12;
				} else if (h > 12) {
					// 18:30pm 18:30am are INVALID_DATE_TIME
					return "";
				}
			} else {
				// no am, pm
				offsetHour = 0;
			}
			h = h+offsetHour;
			return appendZero(h)+":"+appendZero(m);
		}
	}
	return INVALID_DATE_TIME;
}

// @author A0105541U
int Date::parseMonth(std::string st) {
	int i = parseInt(st);
	if (i>=1 && i<=12) return i; 
	i=checkMonthLetter(st);
	if (i>=1 && i<=12) return i; else return -1;
}

// @author A0105541U
std::string Date::parseDate(std::string st) {
	char buffer[BUFFER_SIZE];
	char d[4] = { '/', '.', '-', ' '};
	std::string p[5];
	std::string s = st;
	int y, m ,date;
	int pos;
	bool isValid = false;
	for (int i = 0; i < 4; i++) {
		int num  = 0;
		pos = s.find(d[i]);
		while (pos!=std::string::npos && pos!=0) {
			p[num] = s.substr(0, pos);
			s = s.substr(pos+1);
			pos = s.find(d[i]);
			num ++;
			if (num>2) break;
		}
		p[num] = s;
		if (num >0 && num<=2) {
			for (int j = 0; j<=1; j++) {
				m = parseMonth(p[j]);
				date = parseInt(p[1-j]);
				if (m!=-1 && date!=-1 && date<= DAYS_OF_MONTHS[m-1]) {
					isValid = true;
					break;
				} 
			}

			if (isValid) {
				if (num == 1) {
					y = now().getYear();
				} else {
					y = parseInt(p[2]);
					if (y==-1 || y<MINIMUM_YEAR|| y>=MAXIMUM_YEAR) {
						isValid = false;
					} 
				}
			}

			if (!isValid && num == 2) {
				// try y/m/d
				y = parseInt(p[0]);
				if (y<MINIMUM_YEAR || y>MAXIMUM_YEAR) {
					isValid = false;
				}  else {
					m = parseMonth(p[1]);
					date = parseInt(p[2]);
					if (m!=-1 && date!=-1 && date<= DAYS_OF_MONTHS[m-1]) {
						isValid = true;		
					} 
				}
			}

		} 
		if (isValid) {
			if (m == 2 && date == 29 & !isLeapYear(y)) return INVALID_DATE_TIME;
			sprintf_s(buffer, MONTH_DATE_REP.c_str(), appendZero(m).c_str(), appendZero(date).c_str(), y);
			return buffer;
		}
	}

	// try relative 
	s = st;
	for (int i=0; i<s.length() ;i++)
		if (s[i]>='A' && s[i]<='Z') s[i] = s[i]+32;
	Date today = now();
	today.setHour(0);
	today.setMinute(0);		
	if (s == TODAY) {
		isValid = true;
	} else if (s == TOMORROW) {
		today.addDays(1);
		isValid = true;
	} else if (s == YESTERDAY) {
		today.addDays(-1);
		isValid = true;
	} else if (s == DAY_AFTER_TMR) {
		today.addDays(2);
		isValid = true;
	} else {
	// try Days
		bool next = false;
		if (s.substr(0,NEXT.length()+1) == NEXT) {
			next = true;
			s = s.substr(5);
		}
		if (s[0]>='a' && s[0]<='z')  s[0]-= 32;
		int day = parseDayInWeek(s);
		if (day!=-1) {
			int currentDay = today.getDay();
			int diff = day - currentDay;
			if (next) diff += 7;
			today.addDays(diff);
			isValid = true;
		}
	}

	if (isValid) {
		m = today.getMonth()+1;
		date = today.getDate();
		y = today.getYear();
		if (m == 2 && date == 29 & !isLeapYear(y)) return "";
		sprintf_s(buffer, MONTH_DATE_REP.c_str(), appendZero(m).c_str(), appendZero(date).c_str(), y);
		return buffer;
	}

	return "";
}

// @author A0105541U
int Date::parseDayInWeek(std::string s) {
	for (int i = 0; i<7; i++)
		if (s.compare(DAYS_OF_WEEK[i]) == 0 || s.compare(DAYS_OF_WEEK_SHORT[i]) == 0) {
			return i;
		}
	return -1;
}

// @author A0105541U
void Date::fromString(std::string s){
	try{
		//Check if empty string
		if(s == INVALID_DATE_TIME){
			Date::timeinfo = boost::posix_time::not_a_date_time;
			return;
		}
		//Remove extra spaces " ".
		s = Parser::concatenate(Parser::tokenize(s," "));

		int pos;
		std::string st;
		Date currentDate = now();
		bool f;
		bool isValid = false;
		int y, m, d, h, mi;
		std::string date;
		std::string time = parseTime(s);
		if (time != INVALID_DATE_TIME) {
			y = currentDate.getYear();
			m = currentDate.getMonth()+1;
			d = currentDate.getDate();
			h = parseInt(time.substr(0,2));
			mi = parseInt(time.substr(3,2));
			Date::timeinfo = setAllFields(y, m, d, h, mi);
			isValid = true;
		} else {
			date = parseDate(s);
			if (date !=INVALID_DATE_TIME) {
				m = parseInt(date.substr(0,2));
				d = parseInt(date.substr(3,2));
				y = parseInt(date.substr(6));
				Date::timeinfo = setAllFields(y, m, d, DEFAULT_TIME[0], DEFAULT_TIME[1]);
				isValid = true;
			} else {
				pos = s.find_last_of(' ');
				if (pos != std::string::npos) {
					date = parseDate(s.substr(0, pos));
					time = parseTime(s.substr(pos+1));
					if (date!=INVALID_DATE_TIME && time!=INVALID_DATE_TIME ) {
						m = parseInt(date.substr(0,2));
						d = parseInt(date.substr(3,2));
						y = parseInt(date.substr(6));
						h = parseInt(time.substr(0,2));
						mi = parseInt(time.substr(3,2));
						Date::timeinfo = setAllFields(y, m, d, h, mi);
						isValid = true;
					} 
				}
			}
		}
		if (!isValid) {
			Date::timeinfo = boost::posix_time::not_a_date_time;
		}
	} catch(...){
		Date::timeinfo = boost::posix_time::not_a_date_time;
	}
}
//@author A0101728M
bool  Date::isValid(){
	if(timeinfo != boost::posix_time::ptime())
		return true;
	else
		return false;
}

//Methods for manipulating. Accepts negative numbers for subtracting.
Date Date::addHours(int inputHours){
	boost::posix_time::time_duration originalTime = Date::timeinfo.time_of_day();
	boost::posix_time::time_duration hoursToAdd = boost::posix_time::hours(inputHours);
	boost::posix_time::time_duration newTime = originalTime + hoursToAdd;
	Date::updateTime(newTime);
	return *this;
}

Date Date::addMinutes(int inputMinutes){
	boost::posix_time::time_duration originalTime = Date::timeinfo.time_of_day();
	boost::posix_time::time_duration minutesToAdd = boost::posix_time::minutes(inputMinutes);
	boost::posix_time::time_duration newTime = originalTime + minutesToAdd;
	Date::updateTime(newTime);
	return *this;
}

Date Date::addDays(int inputDays){
	boost::gregorian::date originalDate = Date::timeinfo.date();
	boost::gregorian::date_duration daysToAdd = boost::gregorian::date_duration(long(inputDays));
	boost::gregorian::date newDate = originalDate + daysToAdd;
	Date::updateDate(newDate);
	return *this;
}

Date Date::addWeeks(int inputWeeks){
	return Date::addDays(7 * inputWeeks);
}

Date Date::addYears(int inputYears){
	int originalYear = Date::getYear();
	int newYear = originalYear + inputYears;
	Date::setYear(newYear);
	return *this;
}

//Operators
bool Date::operator > (const Date& rhs){
	return timeinfo > rhs.get();
}

bool Date::operator >= (const Date& rhs){
	return timeinfo >= rhs.get();
}

bool Date::operator < (const Date& rhs){
	return timeinfo < rhs.get();
}

bool Date::operator <= (const Date& rhs){
	return timeinfo <= rhs.get();
}

bool Date::operator == (const Date& rhs){
	return timeinfo == rhs.get();
}

bool Date::operator != (const Date& rhs){
	return timeinfo != rhs.get();
}

bool Date::operator = (const Date& rhs){
	this->timeinfo = rhs.get();
	return true;
}